/*
 *    GeoTools - The Open Source Java GIS Toolkit
 *    http://geotools.org
 *
 *    (C) 2005-2008, Open Source Geospatial Foundation (OSGeo)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.data.shapefile.fid;

import static org.geotools.data.shapefile.files.ShpFileType.FIX;

import java.io.IOException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.util.NoSuchElementException;
import java.util.logging.Logger;
import org.geotools.api.data.FIDReader;
import org.geotools.data.shapefile.files.FileReader;
import org.geotools.data.shapefile.files.ShpFiles;
import org.geotools.data.shapefile.files.StreamLogging;
import org.geotools.data.shapefile.shp.ShapefileReader;
import org.geotools.util.NIOUtilities;
import org.geotools.util.URLs;

/**
 * This object reads from a file the fid of a feature in a shapefile.
 *
 * @author Jesse
 */
public class IndexedFidReader implements FIDReader, FileReader, AutoCloseable {
    private static final Logger LOGGER =
            org.geotools.util.logging.Logging.getLogger(IndexedFidReader.class);
    private ReadableByteChannel readChannel;
    private ByteBuffer buffer;
    private long count;
    private String typeName;
    private boolean done;
    private int removes;
    private int currentShxIndex = -1;
    private long currentId;
    private StringBuilder fidBuilder;

    /** move the reader to the recno-th entry in the file. */
    private long bufferStart = Long.MIN_VALUE;

    StreamLogging streamLogger = new StreamLogging("IndexedFidReader");

    public IndexedFidReader(ShpFiles shpFiles) throws IOException {
        init(shpFiles, shpFiles.getReadChannel(FIX, this));
    }

    public IndexedFidReader(ShpFiles shpFiles, ReadableByteChannel in) throws IOException {
        init(shpFiles, in);
    }

    private void init(ShpFiles shpFiles, ReadableByteChannel in) throws IOException {
        this.readChannel = in;
        boolean initialized = false;
        try {
            doInit(shpFiles);
            initialized = true;
        } finally {
            if (!initialized) {
                try {
                    close();
                } catch (IOException e) {
                    // do nothing
                }
            }
        }
    }

    private void doInit(ShpFiles shpFiles) throws IOException {
        this.typeName = shpFiles.getTypeName() + ".";
        this.fidBuilder = new StringBuilder(typeName);
        streamLogger.open();
        getHeader(shpFiles);

        buffer = NIOUtilities.allocate(IndexedFidWriter.RECORD_SIZE * 1024);
        buffer.position(buffer.limit());
    }

    private void getHeader(ShpFiles shpFiles) throws IOException {
        ByteBuffer buffer = NIOUtilities.allocate(IndexedFidWriter.HEADER_SIZE);
        try {
            ShapefileReader.fill(buffer, readChannel);

            if (buffer.position() == 0) {
                done = true;
                count = 0;

                return;
            }

            buffer.position(0);

            byte version = buffer.get();

            if (version != 1) {
                throw new IOException(
                        "File is not of a compatible version for this reader or file is corrupt.");
            }

            this.count = buffer.getLong();
            this.removes = buffer.getInt();
            if (removes > getCount() / 2) {
                URL url = shpFiles.acquireRead(FIX, this);
                try {
                    URLs.urlToFile(url).deleteOnExit();
                } finally {
                    shpFiles.unlockRead(url, this);
                }
            }
        } finally {
            NIOUtilities.clean(buffer, false);
        }
    }

    /**
     * Returns the number of Fid Entries in the file.
     *
     * @return Returns the number of Fid Entries in the file.
     */
    public long getCount() {
        return count;
    }

    /**
     * Returns the number of features that have been removed since the fid index was regenerated.
     *
     * @return Returns the number of features that have been removed since the fid index was
     *     regenerated.
     */
    public int getRemoves() {
        return removes;
    }

    /**
     * Returns the offset to the location in the SHX file that the fid identifies. This search take
     * logN time.
     *
     * @param fid the fid to find.
     * @return Returns the record number of the record in the SHX file that the fid identifies. Will
     *     return -1 if the fid was not found.
     */
    public long findFid(String fid) throws IOException {
        try {
            int idx = typeName.length(); // typeName already contains the trailing "."
            long desired = -1;
            if (fid.startsWith(typeName)) {
                try {
                    desired = Long.parseLong(fid.substring(idx), 10);
                } catch (NumberFormatException e) {
                    return -1;
                }
            } else {
                return -1;
            }

            if ((desired < 0)) {
                return -1;
            }

            if (desired < count) {
                return search(desired, -1, this.count, desired - 1);
            } else {
                return search(desired, -1, this.count, count - 1);
            }
        } catch (NumberFormatException e) {
            LOGGER.warning("Fid is not recognized as a fid for this shapefile: " + typeName);
            return -1;
        }
    }

    /**
     * Searches for the desired record.
     *
     * @param desired the id of the desired record.
     * @param minRec the last record that is known to be <em>before</em> the desired record.
     * @param maxRec the first record that is known to be <em>after</em> the desired record.
     * @param predictedRec the record that is predicted to be the desired record.
     * @return returns the record number of the feature in the shx file.
     */
    long search(long desired, long minRec, long maxRec, long predictedRec) throws IOException {
        if (minRec == maxRec) {
            return -1;
        }

        goTo(predictedRec);
        hasNext(); // force data reading
        next();
        buffer.limit(buffer.capacity());
        if (currentId == desired) {
            return currentShxIndex;
        }

        if (maxRec - minRec < 10) {
            return search(desired, minRec + 1, maxRec, minRec + 1);
        } else {
            long newOffset = desired - currentId;
            long newPrediction = predictedRec + newOffset;

            if (newPrediction <= minRec) {
                newPrediction = minRec + ((predictedRec - minRec) / 2);
            }

            if (newPrediction >= maxRec) {
                newPrediction = predictedRec + ((maxRec - predictedRec) / 2);
            }

            if (newPrediction == predictedRec) {
                return -1;
            }

            if (newPrediction < predictedRec) {
                return search(desired, minRec, predictedRec, newPrediction);
            } else {
                return search(desired, predictedRec, maxRec, newPrediction);
            }
        }
    }

    @SuppressWarnings("PMD.CloseResource") // FileChannel managed as a field
    public void goTo(long recno) throws IOException {
        assert recno < count;
        if (readChannel instanceof FileChannel) {
            long newPosition =
                    IndexedFidWriter.HEADER_SIZE + (recno * IndexedFidWriter.RECORD_SIZE);
            if (newPosition >= bufferStart + buffer.limit() || newPosition < bufferStart) {
                FileChannel fc = (FileChannel) readChannel;
                fc.position(newPosition);
                buffer.limit(buffer.capacity());
                buffer.position(buffer.limit());
            } else {
                buffer.position((int) (newPosition - bufferStart));
            }
        } else {
            throw new IOException("Read Channel is not a File Channel so this is not possible.");
        }
    }

    @Override
    @SuppressWarnings("PMD.UseTryWithResources") // not instantiated here
    public void close() throws IOException {
        try {
            if (buffer != null) {
                NIOUtilities.clean(buffer, false);
            }
        } finally {
            buffer = null;
            readChannel.close();
            streamLogger.close();
        }
    }

    @Override
    @SuppressWarnings("PMD.CloseResource") // FileChannel managed as a field
    public boolean hasNext() throws IOException {
        if (done) {
            return false;
        }

        if (buffer.position() == buffer.limit()) {
            buffer.position(0);

            FileChannel fc = (FileChannel) readChannel;
            bufferStart = fc.position();
            int read = ShapefileReader.fill(buffer, readChannel);

            if (read != 0) {
                buffer.position(0);
            }
        }

        return buffer.remaining() != 0;
    }

    @Override
    public String next() throws IOException {
        if (!hasNext()) {
            throw new NoSuchElementException(
                    "FID index could not be read; the index may be invalid");
        }

        currentId = buffer.getLong();
        currentShxIndex = buffer.getInt();

        fidBuilder.setLength(typeName.length());
        fidBuilder.append(currentId);
        return fidBuilder.toString();
    }

    /**
     * Returns the record number of the feature in the shx or shp that is identified by the the last
     * fid returned by next().
     *
     * @return Returns the record number of the feature in the shx or shp that is identified by the
     *     the last fid returned by next().
     */
    public int currentSHXIndex() {
        if (currentShxIndex == -1) {
            throw new NoSuchElementException(
                    "Next must be called before there exists a current element.");
        }

        return currentShxIndex;
    }

    /**
     * Returns the index that is appended to the typename to construct the fid.
     *
     * @return the index that is appended to the typename to construct the fid.
     */
    public long getCurrentFIDIndex() {
        return currentId;
    }

    @Override
    public String id() {
        return getClass().getName();
    }
}
